﻿namespace MockingBird.Simulator.Framework.Handlers
{
    using System;
    using System.Diagnostics;
    using System.IO;
    using System.Threading;
    using System.Xml;
    using System.Xml.Xsl;
    using MockingBird.Common.Infrastructure.Logging;
    using MockingBird.Simulator.Framework.Configuration;
    using MockingBird.Simulator.Framework.Exceptions;
    using MockingBird.Simulator.Framework.Interfaces;
    using MockingBird.Simulator.Framework.Persistence;


    public class MessageHandlerBase : IMessageHandler
    {
        #region "protected dependencies and variables"

        protected IResponseBuilder responseBuilder;
        protected ISoapFaultBuilder faultBuilder;
        protected IRequestMessageLogger requestLogger;
        protected IResponseMessageLogger responseLogger;

        //dependencies - interfaces (core)
        protected IExecutionContext context;
        protected ILogger logger;

        //dependencies - siblings
        protected IHandlerConfigurationManager configManager;

        //dependencies - concrete types
        protected ApplicationConfiguration appConfiguration;
        protected OperationNameInferenceEngine opInferenceEngine;

        // 
        protected string responseRoot;
        protected string endpointsRoot;
        protected string configurationFileName;


        #endregion

        #region "properties: default-implementation and to-be-implemented"

        /// <summary>
        /// Read-Only property that returns the SHORT name (without any extensions) of the endpoint that this
        /// instance of the handler is associated with.
        /// </summary>
        public virtual string EndpointName
        {
            get
            {
                if (null == context)
                {
                    throw new InvalidOperationException("no context from which to read endpoint name");
                }
                return context.EndpointName;
            }
        }

        /// <summary>
        /// Read-only property that returns the full name of the endpoint (including extension) that 
        /// this instance of the handler is associated with
        /// </summary>
        public virtual string EndpointFullName
        {
            get
            {
                if (null == context)
                {
                    throw new InvalidOperationException("no context from which to read endpoint full name");
                }
                return context.EndpointFullName;
            }
        }

        /// <summary>
        /// Read-Only Property that returns the name of the operation signified in the current request
        /// </summary>
        public virtual string CurrentOperationName { get; protected set; }

        /// <summary>
        /// Read-Only property that indicates whether the current operation is one way (in which case the 
        /// SoapResponse would be null)
        /// </summary>
        public virtual bool IsCurrentOperationOneWay { get; protected set; }

        /// <summary>
        /// Read-only property that indicates whether the current operation requires a variable response mode 
        /// </summary>
        public virtual bool IsVariableResponseMode { get; protected set; }

        public virtual XmlDocument Request { get; protected set; }
        public virtual XmlDocument Response { get; protected set; }
        public virtual string ResponseFileName { get; protected set; }


        #endregion

        #region "constructor(s)"

        protected MessageHandlerBase()
        {
            System.Diagnostics.Debug.WriteLine(string.Format("MessageHandler.ctor (default) on thread ID: {0}", Thread.CurrentThread.ManagedThreadId));
        }

        protected MessageHandlerBase(ILogger logger, ApplicationConfiguration appConfiguration)
        {
            this.logger = logger;
            this.appConfiguration = appConfiguration;

            if (null != appConfiguration)
            {
                this.endpointsRoot = appConfiguration.EndpointsRoot;
            }
        }

        protected MessageHandlerBase(ILogger logger, IHandlerConfigurationManager configManager, IResponseBuilder responseBuilder, ISoapFaultBuilder faultBuilder, IRequestMessageLogger requestMessageLogger, IResponseMessageLogger responseMessageLogger, ApplicationConfiguration appConfig)
        {
            Debug.WriteLine(string.Format("MessageHandlerBase.ctor on thread ID: {0}", Thread.CurrentThread.ManagedThreadId));

            this.logger = logger;
            this.configManager = configManager;
            this.responseBuilder = responseBuilder;
            this.faultBuilder = faultBuilder;
            this.requestLogger = requestMessageLogger;
            this.responseLogger = responseMessageLogger;
            this.Request = new XmlDocument();
            this.Response = new XmlDocument();
            this.appConfiguration = appConfig;
        }
        #endregion

        #region "interface members"

        public virtual void HandleMessage(IExecutionContext context)
        {
            this.context = context;
            Validate();
            InferCurrentOperationName();
            ProcessRequestMessage();
            LogRequestMessage();
            SetupResponseMessage();
            LogResponseMessage();
            WriteResponseMessageToContext();

        }


        #endregion

        #region default-implementation

        protected virtual void Validate()
        {
            //check for a valid execution context
            if (context == null)
            {
                ThrowException(new ArgumentNullException("ExecutionContext", "ExecutionContext is null."));
            }

            this.logger.Log("MessageHandler:ValidateConfiguration()");

            this.configurationFileName = this.appConfiguration.GetEndpointConfigFile(context.EndpointName);

            this.logger.Log("Configuration file name" + configurationFileName);

            configManager.InitializeConfigurationData(configurationFileName);
            configManager.Validate();
        }

        protected virtual void InferCurrentOperationName()
        {
            this.logger.Log("MessageHandler:InferCurrentOperationName()");

            if (this.opInferenceEngine == null)
            {
                this.opInferenceEngine = new OperationNameInferenceEngine(this.context, this.configManager, this.logger);
            }

            try
            {
                this.CurrentOperationName = this.opInferenceEngine.InferCurrentOperationName();
            }
            catch (Exception e)
            {
                this.ThrowException(new InvalidOperationException(e.Message));
            }
        }

        protected virtual void ProcessRequestMessage()
        {
            this.logger.Log("MessageHandlerBase:ProcessSoapRequestMessage()");
            if (String.IsNullOrEmpty(context.RequestMessage))
            {
                ThrowException(new InvalidOperationException("Request Message is null or empty"));
            }

            // TODO:validate the context request message for an envelope, header and body

            // Now load the request message into the SoapRequest member
            this.Request.LoadXml(this.context.RequestMessage);

        }

        protected virtual void LogRequestMessage()
        {
            this.logger.Log("MessageHandlerBase:LogRequestMessage()");
            this.requestLogger.LogMessage(this.context, this.Request);


        }

        protected virtual void SetupResponseMessage()
        {
            this.logger.Log("MessageHandlerBase:SetupSoapResponseMessage()");
            OperationType operation = LocateCurrentOperation();

            // If this is a one way operation, then we dont need to proceed further
            if (operation.ResponseStrategy == ResponseStrategyType.None)
            {
                this.IsCurrentOperationOneWay = true;
                this.context.IsOneWay = true;
                return;
            }


            // Find the response Item using the response locator. 
            ResponseLocator locator = new ResponseLocator(this.configManager, this.logger);
            ResponseType locatedResponse = locator.FindResponseItemForOperation(operation, this.Request);
            // now, having found the response we act on it
            if (locatedResponse != null)
            {
                this.ActOnResponse(locatedResponse);
            }
            else
            {
                string errorMessage = string.Format("Null response encountered looking for a response for operation named {0}. Please check your config because One way operations do not reach this far in the flow", operation.Name);
                this.logger.Log(errorMessage);
                throw new ApplicationOperationException(errorMessage);
            }

        }

        protected void ActOnResponse(ResponseType response)
        {
            this.logger.Log("MessageHandlerBase:ActOnResponse()");

            //The action now depends on what kind of response behavior is required
            // Soapfaults appear twice. will be changed in v3.0
            switch (response.ResponseBehaviour)
            {
                case ResponseBehaviourType.Standard:
                    if (response.ErrorType.Equals(ErrorType.SoapFault))
                        ExecuteSoapFaultResponse(response);
                    else
                        ExecuteFileBasedResponse(response);
                    break;

                case ResponseBehaviourType.Error:
                    //check if its a SoapFault or a timeout
                    if (response.ErrorType.Equals(ErrorType.SoapFault))
                        ExecuteSoapFaultResponse(response);
                    else
                        ExecuteTimeout();
                    break;

                case ResponseBehaviourType.Variable:
                    break;

                default:
                    break;
            }

        }
        protected virtual void LogResponseMessage()
        {
            this.logger.Log("SoapMessageHandler:LogResponseMessage()");

            if (!this.IsCurrentOperationOneWay)
            {
                this.responseLogger.LogMessage(this.context, this.Response);
            }
        }

        protected virtual void WriteResponseMessageToContext()
        {
            this.logger.Log("SoapMessageHandler:WriteResponseMessageToContext()");
            if (!this.IsCurrentOperationOneWay)
            {
                context.WriteResponse(this.Response);
            }
        }

        protected virtual void ExecuteTimeout()
        {
            if (logger != null)
            {
                this.logger.Log("MessageHandler:ExecuteTimeout()");
            }
            int timeout;
            //set the timeout from configuration
            //Int32.TryParse(((NameValueCollection)ConfigurationManager.GetSection("MockingBird.Configuration/Global")).Get("TimeoutForce"),out timeout);
            Int32.TryParse(appConfiguration.TimeoutForce, out timeout);
            Thread.Sleep(timeout);
            //throw new TimeoutException("");

        }

        protected virtual OperationType LocateCurrentOperation()
        {
            // find could return null
            OperationType op = configManager.Find(CurrentOperationName);

            //if no method could be found then throw an exception          
            if (null == op)
            {
                ThrowException(new ApplicationConfigurationException(String.Format("No Operation could be found in configuration corresponding to operation name : {0} .. If derived from the SoapAction ensure that the format of the URI is correct.", CurrentOperationName)));
            }

            return op;


        }


        protected void ThrowException(Exception e)
        {
            ApplicationOperationException opEx;

            this.logger.LogError(e.Message, e);
            System.Diagnostics.Debug.WriteLine(e.GetBaseException());

            // check the type of exception and wrap it if necessary
            switch (e.GetBaseException().GetType().Name)
            {
                case "ApplicationOperationException":
                case "ApplicationConfigurationException":
                case "HandlerConfigurationException":
                    throw e;
                default:
                    opEx = new ApplicationOperationException(e, e.Message);
                    throw opEx;
            }
        }


        #endregion

        #region "can be overridden in child classes"
        protected virtual void ExecuteFileBasedResponse(ResponseType responseItem)
        {
        }


        protected virtual void ExecuteXsltBasedResponse(ResponseType responseItem)
        {
            this.logger.Log("BasicSoapMessageHandler:HandleRequestXsltBasedResponse()");

            string fullTransformFilePath = GetFullPathOfFileInResponsesRoot(responseItem.FileName);

            XslCompiledTransform xslt = new XslCompiledTransform();
            xslt.Load(fullTransformFilePath);

            XmlDocument requestBody = ExtractSoapBody(Request);

            // The root node that we apply the transform to is the child element of the soap body
            XmlNode nodeToTransform = requestBody.DocumentElement.FirstChild;

            // Perform the transform
            XmlDocument resultOfTransform = new XmlDocument();
            using (XmlWriter writer = resultOfTransform.CreateNavigator().AppendChild())
            {
                xslt.Transform(nodeToTransform, null, writer);
            }

            try
            {
                Response = responseBuilder.BuildResponse(resultOfTransform, null);
            }
            catch (Exception e)
            {
                Response = faultBuilder.BuildSoapFault(e);
            }


        }

        protected virtual void ExecuteSoapFaultResponse(ResponseType responseItem)
        {
            //Find the response Root for the response file
            string responseRoot = this.appConfiguration.GetResponseRootForEndpoint(this.context.EndpointName);
            //Update the responseFileName to be a complete path
            ResponseFileName = Path.Combine(responseRoot, responseItem.FileName);

            faultBuilder = new SoapFaultBuilder(logger);

            Response = faultBuilder.BuildSoapFault(ResponseFileName);

        }


        #endregion

        #region "private methods"
        private string GetFullPathOfFileInResponsesRoot(string filename)
        {
            string responseRoot = String.Format("{0}\\{1}\\responses", this.appConfiguration.EndpointsRoot, context.EndpointName);
            return Path.Combine(responseRoot, filename);
        }

        private static XmlDocument ExtractSoapBody(XmlDocument soapRequest)
        {
            XmlDocument result = new XmlDocument();

            // TODO: this needs to be refactored so that request message handling
            // is encapsulated. The issue here is that in an ordinary ASMX based
            // soap message, the whole envelope is available whereas in the
            // WCF implementation only the internals of the body node are present.
            // For proper future processing we should be using the Message class.
            XmlNodeList xn = soapRequest.SelectNodes("//*[local-name()='Body']");
            if (xn.Count > 0)
            {
                result.LoadXml(xn[0].OuterXml);
            }
            else
            {
                result.LoadXml(soapRequest.OuterXml);
            }

            return result;
        }


        #endregion
    }
}
